﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using NLog;

namespace Stee.CAP8.ACL
{
    #region ACL Authorize Filter
    /// <summary>
    /// Marks Authorization Filter attribute to class or method. It was be implement the <see cref="Attribute"/>, <see cref="IAuthorizationFilter"/> and <see cref="IAsyncAuthorizationFilter"/>
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited =false, AllowMultiple = false)]
    public class ACLAuthorizeAttribute : Attribute, IAuthorizationFilter, IAsyncAuthorizationFilter
    {
        private Dictionary<string, List<string>> _dicRoleTask = null;

        private int _AuthType = 0; 
        private bool _IsAnonymous;
        private string _TaskCode;
        private string[] _TaskList;
        private bool _IsAnd;

        /// <summary>
        /// Set global authorization filter
        /// </summary>
        public ACLAuthorizeAttribute(){ }

        /// <summary>
        /// Set anonymous authorization filter
        /// </summary>
        /// <param name="Anonymous"></param>
        public ACLAuthorizeAttribute(bool Anonymous)
        {
            if(!Anonymous)
            {
                throw new ArgumentNullException($"Anonymous can not be false.");
            }

            _IsAnonymous = Anonymous;
            _AuthType = 1;
        }

        /// <summary>
        /// Set single task authorization filter
        /// </summary>
        /// <param name="Task">specialed task code</param>
        public ACLAuthorizeAttribute(string Task)
        {
            if (string.IsNullOrEmpty(Task))
            {
                throw new ArgumentNullException($"Task can not be empty.");
            }

            _TaskCode = Task;
            _AuthType = 2;
        }

        /// <summary>
        /// Set multiple tasks authorization filter
        /// </summary>
        /// <param name="isAnd">the "or" or "and" of task code list</param>
        /// <param name="TaskList">task code list</param>
        public ACLAuthorizeAttribute(bool isAnd, params string[] TaskList)
        {
            _TaskList = TaskList;
            _IsAnd = isAnd;
            _AuthType = 3;
        }

        /// <summary>
        /// Implement the interface of <see cref="IAuthorizationFilter"/>
        /// </summary>
        /// <param name="context"><see cref="AuthorizationFilterContext"/></param>
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            if (_IsAnonymous || context == null)
            {
                return;
            }

            try
            {
                var user = context.HttpContext.User;
                LogHelper.WriteLog(LogLevel.Info, $"OnAuthorization  start IsAuthenticated:{user.Identity.IsAuthenticated}");
                if (!user.Identity.IsAuthenticated)
                {
                    if (_AuthType == 0)
                    {
                        if (IsUserAuthorized(context.ActionDescriptor as ControllerActionDescriptor))
                        {
                            return;
                        }
                    }

                    context.Result = new UnauthorizedResult();
                }
                else
                {
                    string roleCode = null;
                    try
                    {
                        var claimsIdentity = user.Identity as ClaimsIdentity;
                        roleCode = claimsIdentity.FindFirst(claimsIdentity.RoleClaimType).Value;
                    }
                    catch (NullReferenceException ex)
                    {
                        LogHelper.WriteLog(LogLevel.Error, $"Exception: Cannot get Role code. {typeof(ClaimsIdentity)}", ex);
                    }

                    var chkResult = false;
                    if (!string.IsNullOrEmpty(roleCode))
                    {
                        switch (_AuthType)
                        {
                            case 0:
                                chkResult = IsUserAuthorized(context.ActionDescriptor as ControllerActionDescriptor, roleCode);
                                break;
                            case 2:
                                chkResult = IsUserAuthorized(roleCode, _TaskCode);
                                break;
                            case 3:
                                chkResult = IsUserAuthorized(roleCode, _TaskList, _IsAnd);
                                break;
                        }
                    }

                    if (chkResult)
                    {
                        return;
                    }
                    else
                    {
                        var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
                        context.Result = new JsonResult($"Controller: {descriptor.ControllerName}, Action:{descriptor.ActionName}") { StatusCode = 403 };
                    }
                }
            }
#pragma warning disable CA1031 // 不捕获常规异常类型
            catch (Exception ex)
            {
                LogHelper.WriteLog(LogLevel.Error, $"Exception:{ex.Message}.", ex);
                context.Result = new UnauthorizedResult();
            }
        }

        /// <summary>
        /// Whether to grant permissions to the role 
        /// </summary>
        /// <param name="descriptor"><see cref="ControllerActionDescriptor"/></param>
        /// <param name="roleCode">role</param>
        /// <returns>true/false</returns>
        private bool IsUserAuthorized(ControllerActionDescriptor descriptor, string roleCode = null)
        {
            ACLContractAttribute contractAttribute = null;
            try
            {
                contractAttribute = descriptor.MethodInfo.GetCustomAttribute<ACLContractAttribute>();
                if (contractAttribute == null)
                {
                    contractAttribute = descriptor.ControllerTypeInfo.GetCustomAttribute<ACLContractAttribute>();
                }
            }
            catch(AmbiguousMatchException ex)
            {
                LogHelper.WriteLog(LogLevel.Error, $"{descriptor.MethodInfo.Name}:{ex.Message}.", ex);
            }

            //Cannot get ACL Contract Attribute return
            if (contractAttribute == null)
            {
                return false;
            }

            if(contractAttribute.IsAnonymous)
            {
                return true;
            }


            if (roleCode == null)
            {
                return false;
            }
            else
            {
                var ret = false;
                switch (contractAttribute.AuthType)
                {
                    case 1:
                        ret = IsUserAuthorized(roleCode, contractAttribute.TaskCode);
                        break;
                    case 2:
                        ret = IsUserAuthorized(roleCode, contractAttribute.TaskList, contractAttribute.IsAnd);
                        break;
                }
                return ret;
            }
            
           
        }

        /// <summary>
        /// Whether to grant permissions to the role 
        /// </summary>
        /// <param name="roleCode">role</param>
        /// <param name="taskCode">task</param>
        /// <returns></returns>
        private bool IsUserAuthorized(string roleCode, string taskCode)
        {
            var tasks = GetRoleTask(roleCode);
            return tasks.Any(q => q == taskCode);
        }

        /// <summary>
        /// Whether to grant permissions to the role 
        /// </summary>
        /// <param name="roleCode">role</param>
        /// <param name="taskCodeList">task list</param>
        /// <param name="isAnd">"or" or "and" for the task list</param>
        /// <returns></returns>
        private bool IsUserAuthorized(string roleCode, string[] taskCodeList, bool isAnd)
        {
            var tasks = GetRoleTask(roleCode);
            foreach(var item in taskCodeList)
            {
                if(isAnd)
                {
                    if(!tasks.Any(q => q == item))
                    {
                        return false;
                    }
                }
                else
                {
                    if (tasks.Any(q => q == item))
                    {
                        return true;
                    }
                }
            }

            return isAnd;
        }
       
        /// <summary>
       /// Get task list of the role.
       /// </summary>
       /// <param name="roleCode">role</param>
       /// <returns></returns>
        private List<string> GetRoleTask(string roleCode)
        {
            if (_dicRoleTask == null)
            {
                _dicRoleTask = new Dictionary<string, List<string>>();
            }

            if (_dicRoleTask.ContainsKey(roleCode))
            {
                return _dicRoleTask[roleCode];
            }
            else
            {
                //Get Data from cache or database
                //var someService = context.HttpContext.RequestServices.GetService<ISomeService>();

                //These data for testing only
                var tasks = new List<string>() { "TestTask1", "TestTask2", "TestTask3", "TestTask4", "TestTask5", "TestTask6"};
                _dicRoleTask.Add(roleCode, tasks);
                return tasks;
            }
        }

        /// <summary>
        /// Implement the interface of <see cref="IAsyncAuthorizationFilter"/>
        /// </summary>
        /// <param name="context"><see cref="AuthorizationFilterContext"/></param>
        public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
        {
            if (_IsAnonymous || context == null)
            {
                return;
            }

            try
            {
                var user = context.HttpContext.User;
                LogHelper.WriteLog(LogLevel.Info, $"OnAuthorizationAsync  start IsAuthenticated:{user.Identity.IsAuthenticated}");
                if (!user.Identity.IsAuthenticated)
                {
                    if (_AuthType == 0)
                    {
                        if (IsUserAuthorized(context.ActionDescriptor as ControllerActionDescriptor))
                        {
                            return;
                        }
                    }

                    await context.HttpContext.ChallengeAsync().ConfigureAwait(false);
                    context.Result = new UnauthorizedResult();
                }
                else
                {
                    string roleCode = null;
                    try
                    {
                        var claimsIdentity = user.Identity as ClaimsIdentity;
                        roleCode = claimsIdentity.FindFirst(claimsIdentity.RoleClaimType).Value;
                    }
                    catch(NullReferenceException ex)
                    {
                        LogHelper.WriteLog(LogLevel.Error, $"Exception: Cannot get Role code. {typeof(ClaimsIdentity)}", ex);
                    }
                    
                    var chkResult = false;
                    if (!string.IsNullOrEmpty(roleCode))
                    {
                        switch (_AuthType)
                        {
                            case 0:
                                chkResult = IsUserAuthorized(context.ActionDescriptor as ControllerActionDescriptor, roleCode);
                                break;
                            case 2:
                                chkResult = IsUserAuthorized(roleCode, _TaskCode);
                                break;
                            case 3:
                                chkResult = IsUserAuthorized(roleCode, _TaskList, _IsAnd);
                                break;
                        }
                    }
                    if (chkResult)
                    {
                        return;
                    }
                    else
                    {
                        // await context.HttpContext.ChallengeAsync().ConfigureAwait(false);
                        var descriptor = context.ActionDescriptor as ControllerActionDescriptor;
                        context.Result = new JsonResult($"Controller: {descriptor.ControllerName}, Action:{descriptor.ActionName}") { StatusCode = 403 };
                    }
                }
            }
#pragma warning disable CA1031 // 不捕获常规异常类型
            catch (Exception ex)
            {
                LogHelper.WriteLog(LogLevel.Error, $"Exception:{ex.Message}.", ex);
            }
        }
    }
    #endregion

    #region Global Attribute
    /// <summary>
    /// Marks ACL Contract attribute to class or method. It was be implement the <see cref="Attribute"/>
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
    public class ACLContractAttribute : Attribute
    {
        public int AuthType { get; private set; }
        public bool IsAnonymous { get; private set; }
        public string TaskCode { get; private set; }
        public string[] TaskList { get; private set; }
        public bool IsAnd { get; private set; }

        /// <summary>
        /// Set Anonymous Attribute
        /// </summary>
        public ACLContractAttribute(bool Anonymous)
        {
            if (!Anonymous)
            {
                throw new ArgumentNullException($"Anonymous can not be false.");
            }

            IsAnonymous = Anonymous;
        }

        /// <summary>
        /// Set Task Attribute
        /// </summary>
        public ACLContractAttribute(string Task)
        {
            if (string.IsNullOrEmpty(Task))
            {
                throw new ArgumentNullException($"Task can not be empty.");
            }

            AuthType = 1;
            TaskCode = Task;
        }

        /// <summary>
        /// Set multiple tasks Attribute
        /// </summary>
        public ACLContractAttribute(bool isAnd, params string[] taskList)
        {
            AuthType = 2;
            TaskList = taskList;
            IsAnd = isAnd;
        }
    }
    #endregion
}
